package org.dromara.autoapi.controller;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.db.*;
import cn.hutool.db.meta.MetaUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.dromara.autoapi.model.PageVo;
import org.dromara.autoapi.utils.TwoWordOperation;
import org.dromara.common.core.domain.R;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;

import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/autoapi")
public class AutoApiController {

    private final ConcurrentMap<String, Set<String>> TABLES = new ConcurrentHashMap<>();

    @Autowired
    private ObjectMapper objectMapper;

    private Db db;
    private DataSource dataSource;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
        this.db = new Db(dataSource);
    }

    @PostConstruct
    @GetMapping("/refreshDb")
    public R<Boolean> refreshDb() {
        log.info("*** refreshDb ***");
        try {
            TABLES.clear();
            List<String> list = MetaUtil.getTables(dataSource);
            for (String table : list) {
                String[] names = MetaUtil.getColumnNames(dataSource, table);
                TABLES.put(table, new HashSet<>(Arrays.asList(names)));
            }
            log.info("{}", TABLES);
            return R.ok(true);
        } catch (Exception e) {
            return R.fail(e.getMessage(),false);
        }
    }


    @GetMapping("/tableFields/{entityName}")
    public R<List<String>> getTableFields(@PathVariable String entityName) {
        try {
            entityName = checkTableName(entityName);
            Set<String> names = TABLES.get(entityName);
            List<String> list = names.stream().map(StrUtil::toCamelCase).toList();
            return R.ok(list);
        }catch (Exception e) {
            return R.fail(e.getMessage());
        }
    }

    @PostMapping("/{entityName}/{isAutoIncrement}")
    public R<Long> insert(@PathVariable String entityName, @PathVariable boolean isAutoIncrement, @RequestBody Map<String, Object> entityMap) {
        try {
            entityMap.remove("id");
            entityName = checkTableName(entityName);
            entityMap = checkTableField(entityName, entityMap);
            Entity entity = Entity.parse(entityMap).setTableName(entityName);
            entity.set("id", isAutoIncrement ? null : IdUtil.getSnowflakeNextId());
            return R.ok(db.insertForGeneratedKey(entity));
        } catch (Exception e) {
            return R.fail(e.getMessage());
        }
    }


    @PutMapping("/{entityName}/{id}")
    public R<Integer> updateById(@PathVariable String entityName, @PathVariable Long id, @RequestBody Map<String, Object> entityMap) {
        try {
            entityMap.remove("id");
            entityName = checkTableName(entityName);
            entityMap = checkTableField(entityName, entityMap);
            Entity entity = Entity.parse(entityMap).setTableName(entityName);
            Entity entityId = Entity.create().set("id", id);
            return R.ok(db.update(entity, entityId));
        } catch (Exception e) {
            return R.fail(e.getMessage());
        }
    }


    @DeleteMapping("/{entityName}/{ids}")
    public R<Integer> deleteByIds(@PathVariable String entityName, @PathVariable Long[] ids) {
        try {
            entityName = checkTableName(entityName);
            if (ids == null || ids.length == 0) throw new IllegalArgumentException("ids cannot be null");
            Entity entity = Entity.create(entityName).set("id", ids.length == 1 ? ids[0] : ids);
            return R.ok(db.del(entity));
        } catch (Exception e) {
            return R.fail(e.getMessage());
        }
    }


    @GetMapping("/{entityName}/{id}")
    public R<Map<String,Object>> getById(@PathVariable String entityName, @PathVariable Long id) {
        try {
            entityName = checkTableName(entityName);
            Entity entity = db.get(entityName, "id", id);
            return R.ok(entity == null ? null : entityToCamelCaseMap(entity));
        } catch (Exception e) {
            return R.fail(e.getMessage());
        }
    }


    @GetMapping("/{entityName}")
    public PageVo<Map<String,Object>> pageQuery(@PathVariable String entityName,
                                                        @RequestParam(required = false, name = "where") String whereJson,
                                                        @RequestParam(defaultValue = "true") boolean isDesc,
                                                        @RequestParam(defaultValue = "id") String orderBy,
                                                        @RequestParam(defaultValue = "10") int pageSize,
                                                        @RequestParam(defaultValue = "1") int pageNum) {
        try {
            entityName = checkTableName(entityName);
            orderBy = checkTableField(entityName, orderBy);
            PageResult<Entity> pageResult;
            Map<String, Object> where = null;
            if (StringUtils.hasText(whereJson)) {
                where = objectMapper.readValue(whereJson, Map.class);
            }
            if (where != null && !where.isEmpty()) {
                where = checkTableField(entityName, where);
                List<String> whereSql = TwoWordOperation.toWhereSql(where);
                String sql = """
                        select * from `%s` where %s order by %s %s
                        """.formatted(entityName, whereSql.get(0), orderBy, isDesc ? "desc" : "asc");
                Object[] params = whereSql.subList(1, whereSql.size()).toArray();
                pageResult = db.page(sql, new Page(pageNum-1, pageSize),params);
            } else {
                String sql = """
                        select * from `%s` order by %s %s
                        """.formatted(entityName, orderBy, isDesc ? "desc" : "asc");
                pageResult = db.page(sql, new Page(pageNum-1, pageSize));
            }

            List<Map<String,Object>> list = pageResult.stream().map(this::entityToCamelCaseMap).toList();
            return PageVo.ok(pageResult.getTotal(),list);
        } catch (Exception e) {
            return PageVo.fail(e.getMessage());
        }
    }



    private String checkTableName(String entityName) {
        String tableName;
        if (!StrUtil.isBlank(entityName)) {
            tableName = StrUtil.toUnderlineCase(entityName);
            if (TABLES.containsKey(tableName)) {
                return tableName;
            }
        }
        throw new IllegalArgumentException("不合法的参数 " + entityName);
    }

    private Map<String, Object> checkTableField(String tableName, Map<String, Object> entityMap) {
        Map<String, Object> fieldMap = new HashMap<>();
        Set<String> allFields = TABLES.get(tableName);
        entityMap.forEach((k, v) -> {
            String field = StrUtil.toUnderlineCase(k);
            if (allFields.contains(field)) {
                fieldMap.put(field, v);
            } else {
                throw new IllegalArgumentException("不合法的参数 " + k);
            }
        });
        return fieldMap;
    }

    private String checkTableField(String tableName, String field) {
        String tableField = StrUtil.toUnderlineCase(field);
        Set<String> allFields = TABLES.get(StrUtil.toUnderlineCase(tableName));
        if (allFields.contains(tableField)) {
            return tableField;
        }
        throw new IllegalArgumentException("不合法的参数 " + field);
    }

    private Map<String,Object> entityToCamelCaseMap(Entity entity) {
        Map<String,Object> map = new HashMap<>(entity.size());
        entity.forEach((k, v) -> {
            String camel = StrUtil.toCamelCase(k);
            map.put(camel, v);
        });
        return map;
    }
}
